﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SlimDX.Direct3D9;
using System.Drawing;
using SlimDX;

namespace Gype.UI.D3D9
{
    internal class GdiFontProvider
    {
        public GdiFontProvider(Rendertarget target, string family, int fontSize)
        {
            mTarget = target as Rendertarget9;
            mGdiFont = new System.Drawing.Font(family, fontSize, FontStyle.Bold, GraphicsUnit.Pixel);
            mFontHeight = mGdiFont.Height;
            mVertexShader = new ShaderCompiler<VertexShader>(mTarget.GraphicsDevice, Shaders.Text, "VertexMain", "vs_3_0");
            mPixelShader = new ShaderCompiler<PixelShader>(mTarget.GraphicsDevice, Shaders.Text, "PixelMain", "ps_3_0");

            mMatrixParam = mVertexShader.ConstantTable["orthoProjMatrix"];
            mViewportParam = mVertexShader.ConstantTable["viewportSize"];
            mTransformParam = mVertexShader.ConstantTable["transformMatrix"]; 

            var maxTextureSize = target.GetMaximumTextureSize();
            mTextureSize = Math.Min(Math.Min(maxTextureSize.Height, maxTextureSize.Width), 512);

            var curChars = GetCodepageChars(System.Globalization.CultureInfo.CurrentCulture.TextInfo.ANSICodePage);
            var usChars = GetCodepageChars(437);
            var str = curChars.Union(usChars);
            AllocateNewTexture();

            LoadCharRange(str.Except(new char[] { '\0' }).ToArray());
            LoadWhiteSpaces();
        }

        public void MeasureString(string str, out int width, out int height)
        {
            width = height = 0;
            LoadCharRange(str);
            float w = 0.0f;
            foreach (var c in str)
            {
                if (char.IsWhiteSpace(c) && c != ' ')
                    continue;

                w += mGlyphs[c].CharWidth + 1.5f;
            }

            height = mFontHeight;
            width = (int)w;
        }

        public Rectangle MeasureString(string str)
        {
            int w, h;
            MeasureString(str, out w, out h);
            return new Rectangle(0, 0, w, h);
        }

        public void DrawString(string str, SlimDX.Vector2 position, SlimDX.Vector2 size, FontFlags flags, Brush clr, float scale = 0.8f)
        {
            if (str == "")
                return;

            var dev = mTarget.GraphicsDevice;
            LoadCharRange(str);
            int totalWidth = 0;
            int totalHeight = 0;
            MeasureString(str, out totalWidth, out totalHeight);
            totalWidth = (int)(totalWidth * scale);
            Rectangle oldScissorRect = Rectangle.Empty;
            bool oldScissorEnabled = false;

            if ((flags & FontFlags.NoClip) == 0)
            {
                oldScissorEnabled = dev.GetRenderState(RenderState.ScissorTestEnable) != 0;
                dev.SetRenderState(RenderState.ScissorTestEnable, true);
                oldScissorRect = dev.ScissorRect;
                // transform clip rect to be at the same position if a transform happens in the vertex shader.
                var positionClip = mTarget.Transform.TransformPoint(position);
                var clipSize = size;

                if (oldScissorEnabled)
                {
                    if (oldScissorRect.X > positionClip.X)
                        positionClip.X = oldScissorRect.X;
                    if (oldScissorRect.Y > positionClip.Y)
                        positionClip.Y = oldScissorRect.Y;
                    if (oldScissorRect.Right < (positionClip.X + clipSize.X))
                        clipSize.X = oldScissorRect.Right - positionClip.X;
                    if (oldScissorRect.Bottom < (positionClip.Y + clipSize.Y))
                        clipSize.Y = oldScissorRect.Bottom - positionClip.Y;
                }

                dev.ScissorRect = new Rectangle((int)positionClip.X, (int)positionClip.Y, (int)(clipSize.X), (int)(clipSize.Y));
            }

            Vector2 curPos = position;
            Vector2 endPos = position + size;
            List<UIVertexText> triangleList = new List<UIVertexText>();
            bool isFirstCharOnLine = true;

            foreach (var c in str)
            {
                if (char.IsWhiteSpace(c) && c != ' ' && c != '\n')
                    continue;

                if (c == ' ')
                {
                    // leading whitespaces are ignored, that looks just stupid.
                    if (isFirstCharOnLine)
                        continue;

                    curPos.X += mGlyphs[' '].CharWidth * scale;
                    continue;
                }

                if (c == '\n')
                {
                    if ((flags & FontFlags.LetterWrap) != 0 || (flags & FontFlags.WordWrap) != 0)
                    {
                        isFirstCharOnLine = true;

                        curPos.Y += (mFontHeight * 1.25f) * scale;
                        curPos.X = position.X;

                        if (curPos.Y >= endPos.Y)
                            break;

                        continue;
                    }
                }

                // weve got our first non-whitespace character on this line :)
                isFirstCharOnLine = false;

                var glyph = mGlyphs[c];
                float minX = ((float)(curPos.X - position.X)) / totalWidth;
                float maxX = minX + (glyph.CharWidth / totalWidth);
                if ((flags & FontFlags.BrushPerChar) != 0)
                {
                    minX = 0;
                    maxX = 1;
                }

                if (curPos.X + glyph.CharWidth * scale >= endPos.X)
                {
                    if ((flags & FontFlags.WordWrap) != 0)
                    {
                    }
                    else if ((flags & FontFlags.LetterWrap) != 0)
                    {
                        isFirstCharOnLine = true;

                        curPos.Y += (mFontHeight * 1.25f) * scale;
                        curPos.X = position.X;

                        if (curPos.Y >= endPos.Y)
                            break;
                    }
                }

                var vertices = mTarget.CreateTriangleListText(curPos, new Vector2(glyph.CharWidth * scale, mFontHeight * scale), new Color4(1, 1, 1, 1), new Vector2(minX, 0), new Vector2(maxX, 1));
                vertices[0].TexCoord = glyph.TexCoordTop;
                vertices[1].TexCoord = new Vector2(glyph.TexCoordBottom.X, glyph.TexCoordTop.Y);
                vertices[2].TexCoord = new Vector2(glyph.TexCoordTop.X, glyph.TexCoordBottom.Y);
                vertices[3].TexCoord = vertices[1].TexCoord;
                vertices[4].TexCoord = glyph.TexCoordBottom;
                vertices[5].TexCoord = vertices[2].TexCoord;

                triangleList.AddRange(vertices);
                curPos.X += (glyph.CharWidth + 1.5f) * scale;

                if ((flags & FontFlags.NoClip) == 0 && curPos.X >= endPos.X)
                    break;
            }

            dev.VertexShader = mVertexShader.Shader;
            dev.PixelShader = mPixelShader.Shader;

            mVertexShader.SetValue(mMatrixParam, mTarget.OrthoMatrix);
            mVertexShader.SetValue(mTransformParam, mTarget.Transform);
            mVertexShader.SetValue(mViewportParam, mTarget.Viewport);
            dev.SetTexture(0, mFontTextures[0]);

            clr.ApplyToShader(mPixelShader, new ConstantDescription() { RegisterIndex = 1 });
            dev.VertexDeclaration = mTarget.UIVertexDeclaration;
            if (triangleList.Count >= 3)
                dev.DrawUserPrimitives(PrimitiveType.TriangleList, triangleList.Count / 3, triangleList.ToArray());

            dev.VertexShader = null;
            dev.PixelShader = null;

            if ((flags & FontFlags.NoClip) == 0)
            {
                dev.ScissorRect = oldScissorRect;
                dev.SetRenderState(RenderState.ScissorTestEnable, oldScissorEnabled);
            }
        }

        private char[] GetCodepageChars(int codepage)
        {
            byte[] letters = new byte[223];
            for (int i = 33; i < 256; ++i)
            {
                letters[i - 33] = (byte)i;
            }

            var encoding = Encoding.GetEncoding(codepage);
            var str = encoding.GetChars(letters);
            return str;
        }

        private void LoadWhiteSpaces()
        {
            var size = mBitmapGraphics.MeasureString(" ", mGdiFont);
            mGlyphs.Add(' ',
                new CharGlyphEntry()
                {
                    IgnoreDraw = true,
                    CharWidth = (int)size.Width,
                    TexCoordBottom = Vector2.Zero,
                    TexCoordTop = Vector2.Zero,
                    TextureIndex = -1
                }
                );
        }

        private void LoadCharRange(string range)
        {
            LoadCharRange(range.Distinct().ToArray());
        }

        private void LoadCharRange(char[] chars)
        {
            List<char> newChars = new List<char>();

            foreach (char c in chars)
            {
                if (mGlyphs.ContainsKey(c))
                    continue;

                newChars.Add(c);
            }

            if (newChars.Count == 0)
                return;

            Bitmap bmp = new Bitmap(mFontHeight * 2, mFontHeight, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            Graphics g = Graphics.FromImage(bmp);

            SolidBrush brush = new SolidBrush(Color.White);

            foreach (char c in newChars)
            {
                g.Clear(Color.Transparent);
                g.DrawString(c.ToString(), mGdiFont, brush, PointF.Empty);

                int charStart = mFontHeight * 2;
                int charEnd = 0;
                bool startFound = false;
                bool endFound = false;

                for (int x = 0; x < mFontHeight * 2; ++x)
                {
                    for (int y = 0; y < mFontHeight; ++y)
                    {
                        var clr = bmp.GetPixel(x, y);
                        var clrEnd = bmp.GetPixel(mFontHeight * 2 - 1 - x, y);
                        if (clr.A < 20 && clrEnd.A < 20)
                            continue;

                        if (clr.A >= 20 && !startFound)
                        {
                            charStart = x;
                            startFound = true;
                            if (endFound)
                                break;
                        }

                        if (clrEnd.A >= 20 && !endFound)
                        {
                            charEnd = mFontHeight * 2 - 1 - x;
                            endFound = true;
                            if (startFound)
                                break;
                        }
                    }

                    if (startFound && endFound)
                        break;
                }

                CharGlyphEntry entry = new CharGlyphEntry();

                if (charEnd < charStart)
                {
                    entry.IgnoreDraw = true;
                    mGlyphs.Add(c, entry);
                    continue;
                }

                entry.CharWidth = (charEnd - charStart) + 1;
                entry.TextureIndex = mFontTextures.Count - 1;

                if ((mCurOfsX + (charEnd - charStart) + 3) > mTextureSize)
                {
                    mCurOfsX = 0;
                    mCurOfsY += mFontHeight + 3;
                    if (mCurOfsY + mFontHeight >= mTextureSize)
                    {
                        UpdateTexture();
                        AllocateNewTexture();
                        mCurOfsY = 0;
                    }
                }

                entry.TexCoordTop = new SlimDX.Vector2((float)mCurOfsX / mTextureSize, (float)mCurOfsY / mTextureSize);
                entry.TexCoordBottom = new SlimDX.Vector2((float)(mCurOfsX + (charEnd - charStart) + 1) / mTextureSize, (float)(mCurOfsY + mFontHeight) / mTextureSize);
                entry.IgnoreDraw = false;

                mGlyphs.Add(c, entry);
               
                mBitmapGraphics.DrawImage(bmp, new RectangleF(mCurOfsX, mCurOfsY, (charEnd - charStart) + 1, mFontHeight), new RectangleF(charStart, 0, (charEnd - charStart) + 1, mFontHeight), GraphicsUnit.Pixel);
                mCurOfsX += (charEnd - charStart) + 3;
            }

            UpdateTexture();
            bmp.Dispose();
        }

        private void UpdateTexture()
        {
            var lockData = mCurrentBitmap.LockBits(new Rectangle(0, 0, mTextureSize, mTextureSize), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            var surf = mCurrentTexture.GetSurfaceLevel(0);
            Surface.FromStream(surf, new SlimDX.DataStream(lockData.Scan0, mTextureSize * mTextureSize * 4, true, true), Filter.Default, 0, Format.A8R8G8B8, 4 * mTextureSize, new Rectangle(0, 0, mTextureSize, mTextureSize));
            surf.Dispose();
            mCurrentBitmap.UnlockBits(lockData);
        }

        private void AllocateNewTexture()
        {
            if (mCurrentBitmap != null)
                mBitmapGraphics.Clear(Color.Transparent);
            else
            {
                mCurrentBitmap = new Bitmap(mTextureSize, mTextureSize, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
                mBitmapGraphics = Graphics.FromImage(mCurrentBitmap);
            }

            mCurrentTexture = new Texture(mTarget.GraphicsDevice, mTextureSize, mTextureSize, 1, Usage.None, Format.A8R8G8B8, Pool.Managed);
            mFontTextures.Add(mCurrentTexture);
        }

        private class CharGlyphEntry
        {
            public SlimDX.Vector2 TexCoordTop { get; set; }
            public SlimDX.Vector2 TexCoordBottom { get; set; }
            public int CharWidth { get; set; }
            public int TextureIndex { get; set; }
            public bool IgnoreDraw { get; set; }
        }

        private int mFontHeight = 0;
        private int mCurOfsX = 0;
        private int mCurOfsY = 0;
        private Graphics mBitmapGraphics;
        private Bitmap mCurrentBitmap;
        private Texture mCurrentTexture;
        private Dictionary<char, CharGlyphEntry> mGlyphs = new Dictionary<char, CharGlyphEntry>();
        private List<Texture> mFontTextures = new List<Texture>();
        private int mTextureSize;
        private System.Drawing.Font mGdiFont;
        private Rendertarget9 mTarget;
        private ShaderCompiler<VertexShader> mVertexShader;
        private ShaderCompiler<PixelShader> mPixelShader;
        private ConstantDescription mMatrixParam;
        private ConstantDescription mViewportParam;
        private ConstantDescription mTransformParam;
    }
}
